Amazon SNS トピックに HTTP/HTTPS エンドポイントをサブスクライブする機能を本番運用する前に考えたこと
Amazon SNS を使用して HTTP または HTTPS エンドポイントに通知メッセージを送信できます。
本来であればSQSをサブスクライブしてSQSのポリシーで認証、認可を行うのがシンプルかと思いますが、諸々の都合によりHTTP/HTTPS エンドポイントを直接サブスクライブする場合を前提に調査しました。
より安全に HTTP/HTTPSエンドポイントに対して配信をする場合は SQS, EventBridge Pipes などを経由するとシンプルに認証、認可が行なえ、責任分界点も分離することができると思います。
この機能をプロダクションで運用する上で懸念した点について手元で検証できる点は検証し、手元での検証が難しいものはドキュメントの確認を行いました。
一旦一通り懸念が解消されたので備忘として整理します。
対障害性
エンドポイントからの応答がない場合や、エラー時、リトライの挙動でいくつか懸念がありました。
エンドポイントからの応答がない場合
ドキュメントには接続は 15 秒でタイムアウト扱いになるという記載があります。
これによりメッセージの失敗というステータスになり、配信ポリシーに従ってリトライが行われます。
ドキュメントに記載の通りですが、この後に記載する各種ステータスコードに対してリトライが行われるかの検証のついでに、応答がない場合についてもドキュメントの通りであるかを検証しました。
事前準備としてAPI エンドポイントを用意して POST リクエストがあった場合、30 秒間レスポンスを返さずにその後ステータスコード 200 で返す実装を行いました。
そしてイベントを Publisher で 1 度のみ発生させメッセージを配信して、Lambda のモニタリングタブの Recent invocations にてリトライがなされるか確かめました。 エンドポイントは API Gateway と Lambda で構成しています。
結果としてドキュメント通り配信ポリシーに従い、リトライが行われました。
エラー応答
エンドポイントが 200-4xx の範囲外のステータスコードを返した場合、Amazon SNS はメッセージの配信を失敗とみなし、リトライが行われるという記載がドキュメントにありました。
ドキュメントにおける「失敗」とは、「リトライを行うべき状況であると判断する」と読み替えました。
以下の挙動を検証したく、応答がない場合の検証に使用した環境で特定のステータスコードを返却するようにエンドポイントを実装しました。
200-4xx の範囲外のステータスコードを返した場合、Amazon SNS はメッセージの配信を失敗とみなし、リトライが行われる
結果として以下の挙動になりました。
status code | リトライの有無 | ログに表示される配信ステータス |
---|---|---|
200 | 無 | SUCCESS(成功) |
201 | 無 | SUCCESS(成功) |
202 | 無 | SUCCESS(成功) |
208 | 無 | SUCCESS(成功) |
400 | 無 | SUCCESS(成功) |
401 | 無 | SUCCESS(成功) |
403 | 無 | SUCCESS(成功) |
429 | 無 | SUCCESS(成功) |
500 | リトライ | FAILURE(失敗) |
502 | リトライ | FAILURE(失敗) |
503 | リトライ | FAILURE(失敗) |
504 | リトライ | FAILURE(失敗) |
エンドポイントが Amazon SNS からの HTTP POST メッセージに適切なステータス コードで応答することを確認してください。接続は 15 秒でタイムアウトになります。接続がタイムアウトする前にエンドポイントが応答しない場合、またはエンドポイントが 200 ~ 4 xx の範囲外のステータス コードを返した場合、Amazon SNS はメッセージの配信が失敗したものとみなします。
これによりステータスコードとリトライの挙動については、公式ドキュメントの仕様通りであることを確認できました。
また、証明書のエラーについてはナレッジセンターに記載がありました。
記載によると、配信ステータスとしては失敗扱いになり、リトライポリシーに従いリトライが行われます。
配信ステータスを記録しているログには以下のように記録されます。attemptsは試行回数を意味するので以下の場合は4回リトライが行われたことになります。
"notification": { "messageId": "...", "topicArn": "arn:aws:sns:ap-northeast-1:***:***", "timestamp": "2021-05-12 06:41:20.778" }, "delivery": { "deliveryId": "***", "destination": "https://***", "providerResponse": "SSLPeerUnverifiedException in HttpClient", "dwellTimeMs": 66171, "attempts": 4 }, "status": "FAILURE" }
200-4xxに含まれ、かつ一般的に異常系と言われるステータスコードも配信ステータスについては成功として表現されます。
CloudWatch Logs に表示されるログを見るとそのことが確認できます。
statusCode は 400 で status は "SUCCESS" になっています。
配信ポリシー、リトライの挙動について
Amazon SNS が失敗した HTTP/S エンドポイントへの配信を再試行する方法を定義している配信ポリシーについてまとめます。
SNS トピックに HTTP/S エンドポイントをサブスクライブした場合のリトライのフェーズは以下のように 4 つあります。
- 遅延なしの再試行フェーズ
- バックオフの前フェーズ
- バックオフフェーズ
- バックオフの後フェーズ
https://docs.aws.amazon.com/ja_jp/sns/latest/dg/sns-message-delivery-retries.html
その上でマネジメントコンソールから設定できる項目については以下のような意味をもちます。
- 再試行回数
- メッセージの配信が失敗した場合に、そのメッセージの配信を再試行する最大回数
- 遅延なしの再試行回数
- メッセージの配信が失敗した場合に、遅延なしでそのメッセージの配信を再試行する回数
- 最小遅延時間
- 再試行間隔の最小値(秒)再試行間隔は各再試行後増加するがこの値未満にはならない
- 最大遅延時間
- 再試行間隔の最大値(秒)再試行間隔は各再試行後増加するがこの値を超えることはない
- 最小遅延時間での再試行回数
- 最小遅延時間を適用してメッセージの配信を再試行する回数
- 最大遅延時間での再試行回数
- 最大遅延時間を適用してメッセージの配信を再試行する回数
- 最大受信レート
- エンドポイントが 1 秒あたりに受信できるメッセージの最大数
- バックオフ関数の再実行
- 再試行間隔を計算するための関数
詳細はドキュメントをご確認ください。
設定は JSON としても表現されデフォルトの設定は以下のようになります。
{ "healthyRetryPolicy": { "numRetries": 3, "numNoDelayRetries": 0, "minDelayTarget": 20, "maxDelayTarget": 20, "numMinDelayRetries": 0, "numMaxDelayRetries": 0, "backoffFunction": "linear" }, "requestPolicy": { "headerContentType": "text/plain; charset=UTF-8" } }
デッドレターキュー(Dead letter queue, DLQ)
今回の要件では デッドレターキュー を使用しない想定だったので検証ではなく、ドキュメントを確認するに留まりました。
Amazon SNS ではサブスクリプションに対して DLQ を設定することができます。
これによって SNS がメッセージの配信を再試行した後でもエンドポイントが応答しない場合に、DLQ に送信されます。
冪等性
エンドポイントに対してメッセージが配信されるとして、1 つのメッセージが複数送信され得ることを想定してエンドポイント側で実装を行う必要があるかが懸念でした。
ドキュメントに以下のように記載があります。
ベストエフォート型の重複防止: メッセージは少なくとも 1 回は確実に配信されますが、複数のメッセージのコピーが配信されることもあります。 https://aws.amazon.com/jp/sns/features/
このことから、エンドポイント側は重複したメッセージの配信がシステムに影響を与えないように冪等性を持つよう設計するべきであると理解しました。
また、SQS等を使用せず、SNS とエンドポイント側のみでこれを簡易に担保する場合は、メッセージに含まれるメッセージ ID を使用してメッセージの追跡を行うか、データベースを使用して整合性を保つなどが対応としては考えられそうです。
性能
SNS トピックに HTTP/HTTPS エンドポイントをサブスクライブする際の SNS 側の性能とエンドポイント側の性能について懸念がありました。
遅延時間
エンドポイント側が SNS からのメッセージをさばききれない場合どうすべきかが懸念でしたが、ここまでに記載した再試行(配信ポリシー)やDLQ(Option)でそれを考慮する必要があると想定しています。後述する配信ステータスの記録でメッセージ配信のログを CloudWatch Logs に残すことができます。
スループット
エンドポイントはどれくらいの分間リクエスト(or rps)で構える必要があるか?最大受信レートとの関係が懸念でした。
まず送信する側(SNS)のスペックとしてはドキュメントの Publish API throttlingに記載がありました。
- US East (N. Virginia) Region: 30,000 メッセージ/秒
- US West (Oregon) Region: 9,000 メッセージ/秒
- US West (N. California) Region: 9,000 メッセージ/秒
- Asia Pacific (Tokyo) Region: 9,000 メッセージ/秒
https://docs.aws.amazon.com/general/latest/gr/sns.html
最大受信レートについて
配信ポリシーに項目としてあった最大受信レートについて、これは Amazon SNS が HTTPS エンドポイントに対して送信するメッセージの最大レートを指定するものです。
目的はエンドポイント側が受け取ることができるメッセージの最大数を制御するためで、エンドポイントが過負荷になるのを防ぐために使用するとドキュメントに記載がありました。最大受信レート(maxReceivesPerSecond)への言及は少ないですが、リトライに関するドキュメントで言及があります。
また、この配信ポリシーでは、maxReceivesPerSecond を使用して、配信を毎秒 10 以下に抑えるよう Amazon SNS に指示しています。このセルフスロットリングレートでは、配信された (アウトバウンドトラフィック) よりも多くのメッセージ (インバウンドトラフィック) が公開される可能性があります。インバウンドトラフィックがアウトバウンドトラフィックよりも多い場合、サブスクリプションによって大きなメッセージバックログが蓄積され、メッセージ配信のレイテンシーが高くなる可能性があります。配信ポリシーでは、必ず maxReceivesPerSecond ワークロードに悪影響を与えない値を指定してください。 https://docs.aws.amazon.com/ja_jp/sns/latest/dg/sns-message-delivery-retries.html
この場合設定した最大受信レートを例えば 1 にしたとして、おそらく SNS 側で送信する必要のあるメッセージを溜め込むことになるはずです。その際溜め込んだメッセージが SNS 側で許容できる量を超えた場合の挙動、そもそもそれは起こり得るのか、起こった場合どの時点でエラーとして露見するのかが懸念でした。
ドキュメントなどを調べた結果以下と想定しています。
- maxReceivesPerSecond が低く設定されており、SNS トピックに対して Publish されるメッセージを超過した場合の動作については、SNS サービス側にバックログとしてメッセージが蓄積され、maxReceivesPerSecond のレートにしたがって配信される
- 許容量を超えた場合について、こちらのバックログに上限はなく、メッセージ数や保持期間などによってメッセージが削除されることはない
最大受信レートについては性能試験の実施も検討にあがっていましたが、これをもって SNS トピックに対して Publish されるメッセージを超過した場合の挙動に対する試験は実施しないことになりました。
運用
SNS トピックに HTTP/HTTPS エンドポイントをサブスクライブする機能を運用に乗せる場合に気にしておきたい点についても調べました。
エラー検知、ログ
AWS の仕組みとしては配信ステータスのログ記録にて設定することで検知することができます。
Amazon SNS の配信ステータスのログ機能を有効にすると、 SNS は各メッセージの配信試行についての情報を CloudWatch Logs に記録します。これには、 HTTPS エンドポイントへの配信試行も含まれます。
IAM ロールなど設定が適切に行われれば以下のように配信ステータスのログを保持するロググループが作成され、配信のたびにログが蓄積されます。
ログには以下のような情報が含まれます。
- notification:メッセージ本体です。
- delivery:配信試行に関する情報を含むオブジェクトです。以下のサブ属性を含みます:
- deliveryId:配信試行の一意の識別子です。
- destination:メッセージの送信先(エンドポイントURL)です。
- requestTime:配信試行のリクエストが行われた時刻(ISO 8601形式)です。
- messageId:メッセージの一意の識別子です。
- statusCode:HTTPレスポンスのステータスコードです。メッセージが正常に配信された場合、この値は200になります。
- ndwellTimeMs:メッセージがキューに滞在した時間(ミリ秒)です。
- attempts:配信試行の回数です。
- status:配信のステータスです。SUCCESSまたはFAILUREのいずれかの値を持ちます。
- type:メッセージのタイプです。Notification, SubscriptionConfirmation, UnsubscribeConfirmationのいずれかの値を持ちます。
メッセージが成功した場合は status が "SUCCESS" になっていることが確認できます。
メッセージが失敗した場合は status が "FAILURE" になっていることが確認できます。
このケースは"IO Exception in HttpClient" とあり、これは配信試行中に入出力エラーが発生したことを示しています。 dwellTimeMs はメッセージがキューに滞在した時間(ミリ秒)を示しています。
メッセージ検証
SNS に対して Publisher から送信されたメッセージは SNS により Subscriber、今回だと HTTP/S エンドポイントに対して配信されます。
この時、エンドポイント側に届いたメッセージが本当に正しいものなのか検証する必要があります。
ドキュメントにもメッセージの署名の必要性は記載されています。
悪意のあるユーザーが SNS からのメッセージを装って偽のメッセージを送っていないことを担保する必要があります。特に HTTP/HTTPS エンドポイントは公開状態にしておく必要があるので認証をかけることができません。
エンドポイントに送られたログを見ると、ボディ部は以下のような形式でした
2023-08-02T02:45:33.669Z xxxxxx INFO requestBody: { "Type": "Notification", "MessageId": "xxxxxxxxxx", "TopicArn": "xxxxxx", "Message": "{\"timestamp\":1690944333486,\"event_type\":\"hogehoge\",\"source\":{----}", "Timestamp": "2023-08-02T02:45:33.525Z", "SignatureVersion": "1", "Signature": "xxxx", "SigningCertURL": "https://sns.us-west-2.amazonaws.com/SimpleNotificationService-xxxxxx.pem", "UnsubscribeURL": "https://sns.us-west-2.amazonaws.com/?Action=Unsubscribe&SubscriptionArn=xxx", "MessageAttributes": { "event_type": { "Type": "String", "Value": "hogehoge" } } }
検証の内容としては上記のボディ部からメッセージタイプを確認し、署名バージョンを確認し、署名データを構築します。 そして証明書の URL を確認して証明書をダウンロードして署名を検証することでメッセージが SNS により正しく配信されたものと確認できます。
少し詳細にかくと以下のような手順を想定しています。
- SignatureVersionAmazon
- メッセージ署名バージョンが記載
- Signing CertURL から x509 証明書を取得
- 証明書から公開鍵を抽出
- SigningCertURL メッセージの信頼性と整合性を検証する目的
- メッセージの種類を決定
- Type を参照
- 署名する文字列の作成
- SignatureBase64 形式から値をデコード
- SNS メッセージのハッシュ値を作成
- 公開鍵を使用して SNS メッセージとともに配信された署名を復号化
- 同一であるかを比較
このような手順で SNS メッセージの真正性について検証しますが、SDK の検証はどこまで検証していて、利用者側の責務として検証すべき点はあるのか、というのが懸念としてありました。
弊社の2014 年時点の記事では証明書の URL を確認する部分、記事を引用すると
公開鍵 URL のプロトコルが HTTPS であり、DL 通信に際して正規の CA による証明書による通信が行われることと公開鍵 URL のホスト名が...amazonaws.com であることを確認
は SDK 側でなくエンドポイント側で行う必要があるとして実装を行っていました。
該当の記事で使用しているSignatureChecker のメソッドは Deprecated になっていました。
現在、Java SDK の場合はSnsMessageManager#parseMessageを、Node の場合はaws-js-sns-message-validatorなどを使用する想定です。
SnsMessageManagerのparseMessageのソースコードを読む限り、署名の検証前に前述の確認を行っているようには見えません。
つまりエンドポイント側の責務として、公開鍵URLのプロトコルがHTTPSであり、DL通信に際して正規のCAによる証明書による通信が行われることと公開鍵URLのホスト名が...amazonaws.comであることを確認する必要があると想定しています。
メッセージの検証のタイミング
また、補足ですがメッセージの署名の検証を推奨するケースは 2 つとありました。
- SNS がエンドポイントに対してトピックにサブスクライブしたというメッセージを送信する時
- 2 つ目は Subscribe または Unsubscribe API アクションの実行時に SNS がエンドポイントに確認メッセージを送信する場合
その他, raw メッセージ配信
rawメッセージ配信を有効にすると、SNSから発行されたメッセージからAmazon SNSメタデータが削除され、生のメッセージが送信されます。
Amazon Kinesis Data Firehose または Amazon SQS エンドポイントに対して raw メッセージの配信を有効にすると、発行されたメッセージからすべての Amazon SNS メタデータが削除され、メッセージはそのまま送信されます。
https://docs.aws.amazon.com/ja_jp/sns/latest/dg/sns-large-payload-raw-message-delivery.html
今回の要件ではメタデータを使ってメッセージの真正性を検証するため raw メッセージ配信を有効にすることはないと想定してドキュメントの確認にとどめましたが raw メッセージ配信をあえて利用するのは以下のようなケースを想定しています。
- メッセージの内容が特定のフォーマットである必要がある場合
- エンドポイントが SNS メタデータを理解できない、処理する能力がない場合
- エンドポイントがメッセージ本体だけを必要としている場合
まとめ
記載に誤りなどありましたらご指摘の方をお願い致します。